﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.IO.IsolatedStorage;
using System.Linq;
using System.ServiceModel;
using System.Windows.Threading;
using System.Xml.Linq;
using ICSharpCode.SharpZipLib.Core;
using ICSharpCode.SharpZipLib.Zip;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Metadata;
using Microsoft.Xrm.Sdk.Query;

namespace VisualRibbonEditor
{
	public class Solution
	{
		//collection of all the files in the solution zip.
		private Dictionary<string, byte[]> _files = new Dictionary<string, byte[]>();

		public XElement Customizations { get; set; }

		public XElement RibbonDiffXml { get; set; }

		private List<string> _provisionedLanguages = new List<string>();
		public List<string> ProvisionedLanguages { get { return _provisionedLanguages; } }

		private string _entityName;
		public string EntityName { get { return _entityName; } }
		private Action _loadCompletedCallback = null;
		private Guid? _solutionId;

		private Dispatcher _dispatcher;

		public bool IsLoaded() { return (Customizations != null); }

		public void Load( string entityName, Action loadCompletedCallback)
		{
			this._entityName = entityName;
			this._loadCompletedCallback = loadCompletedCallback;

			_dispatcher = Dispatcher.CurrentDispatcher;
			BackgroundWorker b = new BackgroundWorker();
			b.DoWork += (sender, e) =>
				{
					try
					{
						SetupSolution();
					}
					catch (Exception ex)
					{
						//if error occurs while loading then show error message on main thread.
						Action a = () => { MainWindow.CurrentMainPage.ShowError(ex); };
						_dispatcher.Invoke(a);
					}
				
				};
			b.RunWorkerAsync();
		}

	 
		private void InvokeLoadCompletedCallback()
		{
			_dispatcher.Invoke(_loadCompletedCallback);
		}


		#region Solution Setup, Export, and Saving

		private void ExportSolution()
		{
			//Cause error
			//var svc2 = Helpers.GetOrgService();
			//svc2.RetrieveAsync("blah", Guid.NewGuid(), new ColumnSet(), (x) =>
			//{
			//    int a = 4;
			//});


			SetProgressText("Exporting Solution");

			ServiceWrapper svc = Helpers.GetOrgService();

			OrgRequest req = new OrgRequest("ExportSolution");
			req.Parameters.Add("Managed", false);
			req.Parameters.Add("SolutionName", RIBBON_SOLUTION_NAME);
			
			svc.ExecuteAsync(req.ToOrganizationRequest(), SolutionExportCompleted);
		}

		void SolutionExportCompleted(OrganizationResponse response)
		{
			ParameterCollection c = response.Results;
			
			foreach (var kvp in c)
			{
				if (kvp.Key == "ExportSolutionFile")
				{
					byte[] data = (byte[])kvp.Value;

					MemoryStream s = new MemoryStream(data);
					ExtractZipFile(s);

					byte[] customizationFile = _files["customizations.xml"];
					string customizationFileContent = System.Text.Encoding.UTF8.GetString(customizationFile, 0, customizationFile.Length);
					Customizations = XElement.Parse(customizationFileContent);
					RibbonDiffXml = Customizations.Descendants("RibbonDiffXml").First();
					XElement xLanguages = Customizations.Descendants("Languages").First();
					foreach (XElement xLang in xLanguages.Descendants("Language"))
					{
						_provisionedLanguages.Add(xLang.Value);
					}
				}
			}
			this.InvokeLoadCompletedCallback();
		}


		public string GetIdPrefix()
		{
			XElement firstElementWithId = RibbonDiffXml.Descendants()
				.Where(x => x.Attribute("Id") != null && !x.Attribute("Id").Value.StartsWith("Mscrm")).FirstOrDefault();

			if (firstElementWithId != null)
			{
				string id = firstElementWithId.Attribute("Id").Value;

				int indexOfFirstDot = id.IndexOf('.');
				if (indexOfFirstDot > 0)
				{
					string firstPart = id.Substring(0, indexOfFirstDot);
					if (firstPart != "Mscrm")
					{
						return firstPart;
					}
				}
			}

			return null;
		}



		private void ExtractZipFile(Stream fs)
		{
			ZipFile zf = null;
			try
			{
				zf = new ZipFile(fs);

				foreach (ZipEntry zipEntry in zf)
				{
					if (!zipEntry.IsFile)
					{
						continue;// Ignore directories
					}

					String entryFileName = zipEntry.Name;

					Stream zipStream = zf.GetInputStream(zipEntry);
					
					//StreamReader r = new StreamReader(zipStream);
					//string fileContent = r.ReadToEnd();

					MemoryStream ms = new MemoryStream();
					StreamUtils.Copy(zipStream, ms, new byte[4095]);
					_files.Add(entryFileName, ms.ToArray());

				}

				 
			}
			finally
			{
				if (zf != null)
				{
					zf.IsStreamOwner = true; // Makes close also shut the underlying stream
					zf.Close(); // Ensure we release resources
				}
			}
		}




		public void Save(Action callback)
		{
			BackgroundWorker b = new BackgroundWorker();
			b.DoWork += (sender, e) =>
			{
				try
				{
					byte[] customizationsFile = System.Text.Encoding.UTF8.GetBytes(Customizations.ToString());
					_files["customizations.xml"] = customizationsFile;

					byte[] zipfile = CreateSolutionZipFile();

					Guid jobid = Guid.NewGuid();

					OrgRequest import = new OrgRequest("ImportSolution");
					import.Parameters.Add("CustomizationFile", zipfile);
					import.Parameters.Add("ImportJobId", jobid);
					import.Parameters.Add("PublishWorkflows", true);
					import.Parameters.Add("OverwriteUnmanagedCustomizations", true);

					ServiceWrapper svc = Helpers.GetOrgService();

					SetProgressText("Importing Solution into CRM");

					//Import the solution
					svc.ExecuteAsync(import.ToOrganizationRequest(), (result) =>
					{
						SetProgressText("Publishing Customizations");

						//Then publish customizations
						Microsoft.Crm.Sdk.Messages.PublishXmlRequest publishRequest = new Microsoft.Crm.Sdk.Messages.PublishXmlRequest();
						if (_entityName == "application")
						{
							publishRequest.ParameterXml = "<importexportxml><ribbons><ribbon></ribbon></ribbons></importexportxml>";
						}
						else
						{
							publishRequest.ParameterXml = "<importexportxml><entities><entity>" + _entityName + "</entity></entities></importexportxml>";
						}
						svc.ExecuteAsync(publishRequest, (publishResult) =>
						{
							_dispatcher.Invoke(callback);
						});
					});
				}
				//if error occurs while saving, then show the exception on the main thread
				catch (FaultException<OrganizationServiceFault> ex)
				{
					Action a = () => { MainWindow.CurrentMainPage.ShowError(ex, ex.Detail.Message); };
					_dispatcher.Invoke(a);
				}
				catch (FaultException ex)
				{
					Action a = () => { MainWindow.CurrentMainPage.ShowError(ex, ex.Reason.GetMatchingTranslation().Text); };
					_dispatcher.Invoke(a);
				}
				catch (TimeoutException ex)
				{
					Action a = () => { MainWindow.CurrentMainPage.ShowError(ex, "Timeout has occured"); };
					_dispatcher.Invoke(a);
				}
				catch (Exception ex)
				{
					Action a = () => { MainWindow.CurrentMainPage.ShowError(ex); };
					_dispatcher.Invoke(a);
				}
			};

			b.RunWorkerAsync();
		}

		
	
		private byte[] CreateSolutionZipFile()
		{

			MemoryStream outputMemStream = new MemoryStream();
			ZipOutputStream zipStream = new ZipOutputStream(outputMemStream);

			zipStream.SetLevel(3); //0-9, 9 being the highest level of compression

			foreach (var file in _files)
			{

				ZipEntry newEntry = new ZipEntry(file.Key);
				newEntry.DateTime = DateTime.Now;

				zipStream.PutNextEntry(newEntry);

				zipStream.Write(file.Value, 0, file.Value.Length);
				zipStream.CloseEntry();
			}

			zipStream.IsStreamOwner = false;	// False stops the Close also Closing the underlying stream.
			zipStream.Close();			// Must finish the ZipOutputStream before using outputMemStream.

			outputMemStream.Position = 0;

			byte[] byteArrayOut = outputMemStream.ToArray();
			return byteArrayOut;

		 
		}


		private void WriteFile(string name, byte[] data)
		{
			using (IsolatedStorageFile isolatedStoragFile = IsolatedStorageFile.GetUserStoreForApplication())
			{
				if (isolatedStoragFile.FileExists(name))
				{
					isolatedStoragFile.DeleteFile(name);
				}

				using (IsolatedStorageFileStream stream = new IsolatedStorageFileStream(name, FileMode.Append, isolatedStoragFile))
				{
					stream.Write(data, 0, data.Length);
					stream.Close();
				}
			}
		}


		const string RIBBON_SOLUTION_NAME = "RibbonEditor";

		private EntityMetadata _entityMetaData = null;
		private Guid? _appRibbonId = null;

		private void SetProgressText(string text)
		{
			Action a = () =>
			{
				MainWindow.CurrentMainPage.SetInProgressSubText(text);
			};
			_dispatcher.Invoke(a);
		}

		/// <summary>
		/// Sets up a solution that contains only the entities given.
		/// Note: this will always re-use the same solution and remove or add entities as needed.
		/// </summary>
		private void SetupSolution()
		{

			
			//first, get metadata for the entity.

			if (_entityName == "application")
			{
				SetProgressText("Retrieving application ribbon");
				GetAppRibbon();
			}
			else
			{
				SetProgressText("Retrieving Entity Metadata");
				MetadataCache.GetEntityMetadata(_entityName, RetrievedMetadata);
			}
		}

		private void GetAppRibbon()
		{
			//add the app ribbon to the solution
			QueryExpression getAppRibbonId = new QueryExpression("ribboncustomization");
			getAppRibbonId.Criteria.Conditions.Add(new ConditionExpression("entity", ConditionOperator.Null));
			getAppRibbonId.Criteria.Conditions.Add(new ConditionExpression("ismanaged", ConditionOperator.Equal, false));

			var svc = Helpers.GetOrgService();
			svc.RetrieveMultipleAsync(getAppRibbonId, (ec) =>
				{
					//save the id of the appribbon
					_appRibbonId = ec.Entities[0].Id;

					//next, attempt to retrieve the ribbonEditor solution
					Helpers.GetRecordByName("solution", "uniquename", RIBBON_SOLUTION_NAME, SetupSolution_SolutionRetrieved, "description");
				});
		}
	

		void RetrievedMetadata( EntityMetadata data)
		{
			_entityMetaData = data;

			//next, attempt to retrieve the ribbonEditor solution
			SetProgressText("Checking if RibbonEditor solution exists");
			Helpers.GetRecordByName("solution", "uniquename", RIBBON_SOLUTION_NAME, SetupSolution_SolutionRetrieved, "description");
		}

		void SetupSolution_SolutionRetrieved(Entity solution)
		{
			if (solution == null)
			{
				//solution not created yet, need to create it.
				SetProgressText("Retrieving default publisher");
				//The default publisher has a constant GUID value;
				Guid defaultPublisherId = new Guid("{d21aab71-79e7-11dd-8874-00188b01e34f}");
				Helpers.GetRecordById("publisher", defaultPublisherId, new string[] { "publisherid" }, CreateSolutionThenAddEntityAndExport);
			}
			else
			{
				//the solution HAS been created, but we need to determine if the correct entity is included.
				_solutionId = solution.Id;

				SetupSolutionComponents(_solutionId.Value);
			}
		}

 

		void CreateSolutionThenAddEntityAndExport(Entity publisher)
		{
			SetProgressText("Creating RibbonEditor solution");
			
			//create a new solution
			Entity sol = new Entity() { LogicalName = "solution" };
			sol["uniquename"] = RIBBON_SOLUTION_NAME;
			sol["friendlyname"] = RIBBON_SOLUTION_NAME;
			sol["publisherid"] = new EntityReference() { LogicalName = "publisher", Id = publisher.Id };
			sol["version"] = "1.0";
			sol["description"] = "Solution used by Visual Ribbon Editor";

			var svc = Helpers.GetOrgService();
			svc.CreateAsync(sol, (id) => AddEntityToSolutionThenExport());
		}

		private void SetupSolutionComponents(Guid solutionId)
		{
			SetProgressText("Analyzing RibbonEditor solution");

			Helpers.GetRecordsWithCondition("solutioncomponent", "solutionid", ConditionOperator.Equal, solutionId,
				RetrievedSolutionComponents, "solutioncomponentid", "componenttype", "objectid");
		}

	 

		void RetrievedSolutionComponents(EntityCollection ec)
		{
			//Any entity that is in the solution but not in our list of entities should be removed from the solution

			List<Entity> componentsToRemove = new List<Entity>();
			bool needToAddCurrentEntity = true;
			
			foreach (Entity comp in ec.Entities)
			{
				OptionSetValue componentType = (OptionSetValue) comp["componenttype"];
				Guid? objectId = (Guid?) comp["objectid"];

				if (componentType.Value == 50)
				{
					//APP ribbon
					if (_entityName == "application")
					{
						//the current entity is 'application' and the app ribbon is already in the solution.
						needToAddCurrentEntity = false;
					}
					else
					{
						//dont need appribbon in solution. remove it.
						componentsToRemove.Add(comp);
					}
				}
				else
				{
					//Entity
					if (_entityName == "application")
					{
						//if editing appribbon, then remove all entities from solution
						componentsToRemove.Add(comp);
					}
					else
					{
						//otherwise, remove all un-needed entities, keep the entity that matches _entitymetadata.
						if (objectId == _entityMetaData.MetadataId)
						{
							needToAddCurrentEntity = false;
						}
						else
						{
							componentsToRemove.Add(comp);
						}
					}
				}
			}//endforeach

			//remove existing solution components...


			SetProgressText("Removing un-needed components from solution");

			RemoveMultipleComponents(componentsToRemove, () =>
			{
				//then add current entity to solution...
				if( needToAddCurrentEntity)
				{
					
					SetProgressText("Adding " + _entityName + " to RibbonEditor solution");
					AddEntityToSolutionThenExport();
				}
				else
				{
					//or just export if solution already has entity.
					ExportSolution();
				}
			});

		}

		private void RemoveMultipleComponents( IEnumerable<Entity> components, Action callback)
		{
			if (components.Count() == 0)
			{
				callback();
			}
			else
			{
				Entity component = components.First();
				Guid? objectId = (Guid?) component["objectid"];
				OptionSetValue componentType = (OptionSetValue) component["componenttype"];
				
				OrgRequest req = new OrgRequest("RemoveSolutionComponent");
				req.Parameters.Add("ComponentId", objectId.Value);
				req.Parameters.Add("ComponentType", componentType.Value);
				req.Parameters.Add("SolutionUniqueName", RIBBON_SOLUTION_NAME);

				var svc = Helpers.GetOrgService();
				svc.ExecuteAsync(req.ToOrganizationRequest(), (r) =>
					{
						RemoveMultipleComponents( components.Skip(1), callback);
					});
			}
		}

		private void AddEntityToSolutionThenExport()
		{
			Guid comonentToAdd;
			int componentType;
			if (_entityName == "application")
			{
				comonentToAdd = _appRibbonId.Value;
				componentType = 50;
			}
			else
			{
				comonentToAdd = _entityMetaData.MetadataId.Value;
				componentType = 1;
			}

			OrgRequest req = new OrgRequest("AddSolutionComponent");
			req.Parameters.Add("AddRequiredComponents", false);
			req.Parameters.Add("ComponentId", comonentToAdd);
			req.Parameters.Add("ComponentType", componentType);
			req.Parameters.Add("SolutionUniqueName", RIBBON_SOLUTION_NAME);

			var svc = Helpers.GetOrgService();
			svc.ExecuteAsync(req.ToOrganizationRequest(), (response) =>
			{
				ExportSolution();
			});
		}
 
		


		#endregion

	}
}
